/*
 * #%L
 * Alfresco Data model classes
 * %%
 * Copyright (C) 2005 - 2023 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.service.cmr.repository.datatype;

import java.io.IOException;
import java.io.Serializable;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Calendar;
import java.util.Date;

import org.alfresco.api.AlfrescoPublicApi;
import org.alfresco.util.CachingDateFormat;

/**
 * This data type represents duration/interval/period as defined by the XMLSchema type duration.
 * 
 * The lexical representation of duration is PnYnMnDTnHnMnS.
 * 
 * P is a literal value that starts the expression nY is an integer number of years followed by the literal Y nM is an integer number of months followed by the literal M nD is an integer number of days followed by the literal D T is the literal that separates the date and time nH is an integer number of hours followed by a literal H nM is an integer number of minutes followed by a literal M nS is a decimal number of seconds followed by a literal S
 * 
 * Any numbers and designator may be absent if the value is zero. A minus sign may appear before the literal P to indicate a negative duration. If no time items are present the literal T must not appear.
 * 
 * 
 * This implementation is immutable and thread safe.
 * 
 * There are two forms of duration common on database types. The code contains warnings wheer these are relevant.
 *
 * @author andyh
 */
@AlfrescoPublicApi
public class Duration implements Comparable, Serializable
{

    static final long serialVersionUID = 3274526442325176068L;

    public static final String XML_DAY = "P1D";
    public static final String XML_WEEK = "P7D";
    public static final String XML_TWO_WEEKS = "P14D";
    public static final String XML_MONTH = "P1M";
    public static final String XML_QUARTER = "P3M";
    public static final String XML_SIX_MONTHS = "P6M";
    public static final String XML_YEAR = "P1Y";

    public static final Duration DAY = new Duration(XML_DAY);
    public static final Duration WEEK = new Duration(XML_WEEK);
    public static final Duration TWO_WEEKS = new Duration(XML_TWO_WEEKS);
    public static final Duration MONTH = new Duration(XML_MONTH);
    public static final Duration QUARTER = new Duration(XML_QUARTER);
    public static final Duration SIX_MONTHS = new Duration(XML_SIX_MONTHS);
    public static final Duration YEAR = new Duration(XML_YEAR);

    private static final String s_parse = "-PYMDTHmS";

    private boolean m_positive = true;
    private int m_years = 0;
    private int m_months = 0;
    private int m_days = 0;
    private int m_hours = 0;
    private int m_mins = 0;
    private int m_seconds = 0;
    private int m_nanos = 0;

    // Date duration arithmetic

    /**
     * Add a duration to a date and return the date plus the specified increment.
     * 
     * @param date
     *            - the initial date
     * @param duration
     *            - the duration to add on to the date (the duration may be negative)
     * @return the adjusted date.
     */
    public static Date add(Date date, Duration duration)
    {
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        c.add(Calendar.YEAR, (duration.m_positive ? 1 : -1) * duration.m_years);
        c.add(Calendar.MONTH, (duration.m_positive ? 1 : -1) * duration.m_months);
        c.add(Calendar.DATE, (duration.m_positive ? 1 : -1) * duration.m_days);
        c.add(Calendar.HOUR_OF_DAY, (duration.m_positive ? 1 : -1) * duration.m_hours);
        c.add(Calendar.MINUTE, (duration.m_positive ? 1 : -1) * duration.m_mins);
        c.add(Calendar.SECOND, (duration.m_positive ? 1 : -1) * duration.m_seconds);
        c.add(Calendar.MILLISECOND, (duration.m_positive ? 1 : -1) * duration.m_nanos / 1000000);
        return c.getTime();
    }

    /**
     * Subtract a period for a given date
     * 
     * @param date
     *            - the intial date
     * @param duration
     *            - the diration to subtract
     * @return the adjusted date.
     */

    public static Date subtract(Date date, Duration duration)
    {
        return add(date, duration.unaryMinus());
    }

    /**
     * Constructor for Duration - a zero value duration
     */

    public Duration()
    {
        super();
    }

    /**
     * Construct a Duration from the XMLSchema definition
     */

    public Duration(String duration)
    {

        if (duration.equals("P"))
        {
            throw new RuntimeException("Invalid period: P");
        }

        if (!duration.startsWith("P") && !duration.startsWith("-P"))
        {
            throw new RuntimeException("Invalid period: must start with P or -P");
        }
        else
        {
            boolean dateMode = true;
            int last = -1;
            Double nval = null;
            StringReader reader = new StringReader(duration);
            StreamTokenizer tok = new StreamTokenizer(reader);
            tok.resetSyntax();
            tok.eolIsSignificant(true);
            tok.parseNumbers();
            tok.ordinaryChars('-', '-');
            tok.ordinaryChars('P', 'P');
            tok.ordinaryChars('Y', 'Y');
            tok.ordinaryChars('M', 'M');
            tok.ordinaryChars('D', 'D');
            tok.ordinaryChars('T', 'T');
            tok.ordinaryChars('H', 'H');
            tok.ordinaryChars('m', 'm');
            tok.ordinaryChars('S', 'S');

            int token;
            try
            {
                while ((token = tok.nextToken()) != StreamTokenizer.TT_EOF)
                {
                    if (token == StreamTokenizer.TT_NUMBER)
                    {
                        nval = Double.valueOf(tok.nval);
                    }
                    else if (token == StreamTokenizer.TT_EOF)
                    {
                        throw new RuntimeException("Invalid EOF in Duration");
                    }
                    else if (token == StreamTokenizer.TT_EOL)
                    {
                        throw new RuntimeException("Invalid EOL in Duration");
                    }
                    else if (token == StreamTokenizer.TT_WORD)
                    {
                        throw new RuntimeException("Invalid text in Duration: " + tok.sval);
                    }
                    else
                    {
                        if (tok.ttype == '-')
                        {
                            last = checkIndex(last, "-");
                            m_positive = false;
                        }
                        else if (tok.ttype == 'P')
                        {
                            last = checkIndex(last, "P");
                            // nothing
                        }
                        else if (tok.ttype == 'Y')
                        {
                            last = checkIndex(last, "Y");
                            if (nval != null)
                            {
                                m_years = nval.intValue();
                            }
                            else
                            {
                                throw new RuntimeException("IO Error parsing Duration: " + duration);
                            }
                            nval = null;
                        }
                        else if (tok.ttype == 'M')
                        {
                            if (dateMode)
                            {
                                last = checkIndex(last, "M");
                                if (nval != null)
                                {
                                    m_months = nval.intValue();
                                }
                                else
                                {
                                    throw new RuntimeException("IO Error parsing Duration: " + duration);
                                }
                                nval = null;
                            }
                            else
                            {
                                last = checkIndex(last, "m");
                                if (nval != null)
                                {
                                    m_mins = nval.intValue();
                                }
                                else
                                {
                                    throw new RuntimeException("IO Error parsing Duration: " + duration);
                                }
                                nval = null;
                            }
                        }
                        else if (tok.ttype == 'D')
                        {
                            last = checkIndex(last, "D");
                            if (nval != null)
                            {
                                m_days = nval.intValue();
                            }
                            else
                            {
                                throw new RuntimeException("IO Error parsing Duration: " + duration);
                            }
                            nval = null;
                        }
                        else if (tok.ttype == 'T')
                        {
                            last = checkIndex(last, "T");
                            dateMode = false;
                            nval = null;
                        }
                        else if (tok.ttype == 'H')
                        {
                            last = checkIndex(last, "H");
                            if (nval != null)
                            {
                                m_hours = nval.intValue();
                            }
                            else
                            {
                                throw new RuntimeException("IO Error parsing Duration: " + duration);
                            }
                            nval = null;
                        }
                        else if (tok.ttype == 'S')
                        {
                            last = checkIndex(last, "S");
                            if (nval != null)
                            {
                                m_seconds = nval.intValue();
                                m_nanos = (int) ((long) (nval.doubleValue() * 1000000000) % 1000000000);
                            }
                            else
                            {
                                throw new RuntimeException("IO Error parsing Duration: " + duration);
                            }
                            nval = null;
                        }
                        else
                        {
                            throw new RuntimeException("IO Error parsing Duration: " + duration);
                        }
                    }
                }
            }
            catch (IOException e)
            {
                throw new RuntimeException("IO Error parsing Duration: " + duration);
            }
            catch (RuntimeException e)
            {
                throw new RuntimeException("IO Error parsing Duration: " + duration, e);
            }
        }
    }

    /**
     * Simple index to check identifiers appear in order
     */

    private int checkIndex(int last, String search)
    {
        if ((search == null) || (search.length() == 0))
        {
            throw new RuntimeException("Null or zero length serach");
        }
        int index = s_parse.indexOf(search);
        if (index > last)
        {
            return index;
        }
        else
        {
            throw new RuntimeException("Illegal position for identifier " + search);
        }
    }

    /**
     * Create a duration given a date. The duration is between the two dates provided.
     * 
     * Sadly, it works out the duration by incrementing the lower calendar until it matches the higher.
     */

    public Duration(Date date)
    {
        this(date, new Date());
    }

    /**
     * Create a duration betweeen two dates expressed as strings. Uses the standard XML date form.
     * 
     * @param start
     *            - the date at the start of the period
     * @param end
     *            - the date at the end of the period
     */

    public Duration(String start, String end)
    {
        this(parseDate(start), parseDate(end));
    }

    /**
     * Helper method to parse eaets from strings
     * 
     * @param stringDate
     *            String
     * @return Date
     */
    private static Date parseDate(String stringDate)
    {
        DateFormat df = CachingDateFormat.getDateFormat();
        df.setLenient(true);
        Date date;

        ParsePosition pp = new ParsePosition(0);
        date = df.parse(stringDate, pp);
        if ((pp.getIndex() < stringDate.length()) || (date == null))
        {
            date = new Date();
        }
        return date;

    }

    /**
     * Construct a preiod between the two given dates
     * 
     * @param start_in
     *            Date
     * @param end_in
     *            Date
     */
    public Duration(Date start_in, Date end_in)
    {
        boolean positive = true;
        Date start;
        Date end;
        if (end_in.before(start_in))
        {
            start = end_in;
            end = start_in;
            positive = false;
        }
        else
        {
            start = start_in;
            end = end_in;
            positive = true;
        }
        Calendar cstart = Calendar.getInstance();
        cstart.setTime(start);
        Calendar cend = Calendar.getInstance();
        cend.setTime(end);

        int millis = cend.get(Calendar.MILLISECOND) - cstart.get(Calendar.MILLISECOND);
        if (millis < 0)
        {
            millis += cstart.getActualMaximum(Calendar.MILLISECOND) + 1;
        }
        cstart.add(Calendar.MILLISECOND, millis);

        int seconds = cend.get(Calendar.SECOND) - cstart.get(Calendar.SECOND);
        if (seconds < 0)
        {
            seconds += cstart.getActualMaximum(Calendar.SECOND) + 1;
        }
        cstart.add(Calendar.SECOND, seconds);

        int minutes = cend.get(Calendar.MINUTE) - cstart.get(Calendar.MINUTE);
        if (minutes < 0)
        {
            minutes += cstart.getActualMaximum(Calendar.MINUTE) + 1;
        }
        cstart.add(Calendar.MINUTE, minutes);

        int hours = cend.get(Calendar.HOUR_OF_DAY) - cstart.get(Calendar.HOUR_OF_DAY);
        if (hours < 0)
        {
            hours += cstart.getActualMaximum(Calendar.HOUR_OF_DAY) + 1;
        }
        cstart.add(Calendar.HOUR_OF_DAY, hours);

        int days = cend.get(Calendar.DAY_OF_MONTH) - cstart.get(Calendar.DAY_OF_MONTH);
        if (days < 0)
        {
            days += cstart.getActualMaximum(Calendar.DAY_OF_MONTH) + 1;
        }
        cstart.add(Calendar.DAY_OF_MONTH, days);

        int months = cend.get(Calendar.MONTH) - cstart.get(Calendar.MONTH);
        if (months < 0)
        {
            months += cstart.getActualMaximum(Calendar.MONTH) + 1;
        }
        cstart.add(Calendar.MONTH, months);

        int years = cend.get(Calendar.YEAR) - cstart.get(Calendar.YEAR);
        // cstart.add(Calendar.YEAR, years);

        m_positive = positive;
        m_years = years;
        m_months = months;
        m_days = days;
        m_hours = hours;
        m_mins = minutes;
        m_seconds = seconds;
        m_nanos = millis * 1000000;

    }

    /**
     * Construct a duration from months seconds and nanos Checks sign and fixes up seconds and nano. Treats year-month abd day-sec as separate chunks
     */

    public Duration(boolean positive_in, long months_in, long seconds_in, long nanos_in)
    {

        boolean positive = positive_in;
        long months = months_in;
        long seconds = seconds_in + nanos_in / 1000000000;
        long nanos = nanos_in % 1000000000;

        // Fix up seconds and nanos to be of the same sign

        if ((seconds > 0) && (nanos < 0))
        {
            seconds -= 1;
            nanos += 1000000000;
        }
        else if ((seconds < 0) && (nanos > 0))
        {
            seconds += 1;
            nanos -= 1000000000;
        }

        // seconds and nanos now the same sign - sum to test overall sign

        if ((months < 0) && (seconds + nanos < 0))
        {
            // switch sign
            positive = !positive;
            months = -months;
            seconds = -seconds;
            nanos = -nanos;
        }
        else if ((months == 0) && (seconds + nanos < 0))
        {
            // switch sign
            positive = !positive;
            months = -months;
            seconds = -seconds;
            nanos = -nanos;
        }
        else if ((months > 0) && (seconds + nanos < 0))
        {
            throw new RuntimeException("Can not convert to period - incompatible signs for year_to_momth and day_to_second elements");
        }
        else if ((months < 0) && (seconds + nanos > 0))
        {
            throw new RuntimeException("Can not convert to period - incompatible signs for year_to_momth and day_to_second elements");
        }
        else
        {
            // All +ve
        }

        m_positive = positive;
        m_years = (int) (months / 12);
        m_months = (int) (months % 12);

        m_days = (int) (seconds / (3600 * 24));
        seconds -= m_days * 3600 * 24;
        m_hours = (int) (seconds / 3600);
        seconds -= m_hours * 3600;
        m_mins = (int) (seconds / 60);
        seconds -= m_mins * 60;
        m_seconds = (int) seconds;
        m_nanos = (int) nanos;

    }

    // Duration arithmetic

    /**
     * Add two durations together
     */

    public Duration add(Duration add)
    {

        long months = (this.m_positive ? 1 : -1) * this.getTotalMonths() + (add.m_positive ? 1 : -1) * add.getTotalMonths();
        long seconds = (this.m_positive ? 1 : -1) * this.getTotalSeconds() + (add.m_positive ? 1 : -1) * add.getTotalSeconds();
        long nanos = (this.m_positive ? 1 : -1) * this.getTotalNanos() + (add.m_positive ? 1 : -1) * add.getTotalNanos();

        Duration result = new Duration(true, months, seconds, nanos);
        return result;
    }

    /**
     * Subtract one duration from another
     */

    public Duration subtract(Duration sub)
    {
        long months = (this.m_positive ? 1 : -1) * this.getTotalMonths() - (sub.m_positive ? 1 : -1) * sub.getTotalMonths();
        long seconds = (this.m_positive ? 1 : -1) * this.getTotalSeconds() - (sub.m_positive ? 1 : -1) * sub.getTotalSeconds();
        long nanos = (this.m_positive ? 1 : -1) * this.getTotalNanos() - (sub.m_positive ? 1 : -1) * sub.getTotalNanos();
        Duration result = new Duration(true, months, seconds, nanos);
        return result;
    }

    /**
     * Negate the duration
     */

    public Duration unaryMinus()
    {
        Duration result = new Duration(!this.m_positive, this.getTotalMonths(), this.getTotalSeconds(), this.getTotalNanos());
        return result;
    }

    /**
     * Divide the duration - if year-month drops the day-second part of the duration
     */

    public Duration divide(int d)
    {
        if (isYearToMonth())
        {
            long months = getTotalMonths();
            months /= d;
            Duration result = new Duration(m_positive, months, 0, 0);
            return result;
        }
        else
        {
            long seconds = getTotalSeconds();
            long nanos = (seconds * (1000000000 / d)) % 1000000000;
            nanos += getTotalNanos() / d;
            seconds /= d;
            Duration result = new Duration(m_positive, 0, seconds, nanos);
            return result;
        }
    }

    /**
     * Helper method to get the total number of months - year-month
     */

    private long getTotalMonths()
    {
        return m_years * 12 + m_months;
    }

    /**
     * Helper method to get the total number of seconds
     */

    private long getTotalSeconds()
    {
        return m_seconds + m_mins * 60 + m_hours * 3600 + m_days * 3600 * 24;
    }

    /**
     * Helper method to get the total number of nanos (does not include seconds_
     */

    private long getTotalNanos()
    {
        return m_nanos;
    }

    /**
     * Check if is year-month
     */

    public boolean isYearToMonth()
    {
        return (m_years != 0) || (m_months != 0);
    }

    /**
     * Check if is day-sec
     */

    public boolean isDayToSec()
    {
        return ((m_years == 0) && (m_months == 0));
    }

    /**
     * Check if it includes time
     */

    public boolean hasTime()
    {
        return (m_hours != 0) || (m_mins != 0) || (m_seconds != 0) || (m_nanos != 0);
    }

    /**
     * Extract the year to month part
     */

    public Duration getYearToMonth()
    {
        Duration result = new Duration(m_positive, getTotalMonths(), 0, 0);
        return result;
    }

    /**
     * Extract the day to sec part.
     */

    public Duration getDayToYear()
    {
        Duration result = new Duration(m_positive, 0, getTotalSeconds(), getTotalNanos());
        return result;
    }

    /**
     * Compare two durations
     */

    public int compareTo(Object o)
    {
        if (!(o instanceof Duration))
        {
            throw new RuntimeException("Can not compare Duration and " + o.getClass().getName());
        }

        Duration d = (Duration) o;
        if (this.m_positive != d.m_positive)
        {
            return (m_positive ? 1 : -1);
        }

        if (this.getTotalMonths() != d.getTotalMonths())
        {
            return (m_positive ? 1 : -1) * ((int) (this.getTotalMonths() - d.getTotalMonths()));
        }
        else if (this.getTotalSeconds() != d.getTotalSeconds())
        {
            return (m_positive ? 1 : -1) * ((int) (this.getTotalSeconds() - d.getTotalSeconds()));
        }
        else if (this.getTotalNanos() != d.getTotalNanos())
        {
            return (m_positive ? 1 : -1) * ((int) (this.getTotalNanos() - d.getTotalNanos()));
        }
        else
        {
            return 0;
        }
    }

    /**
     * @see java.lang.Object#equals(Object)
     */

    public boolean equals(Object o)
    {
        if (this == o)
            return true;
        if (!(o instanceof Duration))
            return false;
        Duration d = (Duration) o;
        return (this.m_positive == d.m_positive) && (this.getTotalMonths() == d.getTotalMonths()) && (this.getTotalSeconds() == d.getTotalSeconds()) && (this.getTotalNanos() == d.getTotalNanos());

    }

    /**
     * @see java.lang.Object#hashCode()
     */

    public int hashCode()
    {
        int hash = 17;
        hash = 37 * hash + (m_positive ? 1 : -1);
        hash = 37 * hash + (int) getTotalMonths();
        hash = 37 * hash + (int) getTotalSeconds();
        hash = 37 * hash + (int) getTotalNanos();
        return hash;
    }

    /**
     * Produce the XML Schema string
     * 
     * @see java.lang.Object#toString()
     */

    public String toString()
    {
        StringBuffer buffer = new StringBuffer(128);
        if (!m_positive)
        {
            buffer.append("-");
        }
        buffer.append("P");
        // Always include years as just P on its own is invalid
        buffer.append(m_years).append("Y");

        if (m_months != 0)
        {
            buffer.append(m_months).append("M");
        }
        if (m_days != 0)
        {
            buffer.append(m_days).append("D");
        }
        if (hasTime())
        {
            buffer.append("T");
            if (m_hours != 0)
            {
                buffer.append(m_hours).append("H");
            }
            if (m_mins != 0)
            {
                buffer.append(m_mins).append("M");
            }
            if ((m_seconds != 0) || (m_nanos != 0))
            {
                BigDecimal a = new BigDecimal(m_seconds);
                BigDecimal b = new BigDecimal(m_nanos);
                a = a.add(b.divide(new BigDecimal(1000000000), 9, BigDecimal.ROUND_HALF_EVEN));
                NumberFormat nf = NumberFormat.getInstance();
                buffer.append(nf.format(a));
                buffer.append("S");
            }

        }

        return buffer.toString();
    }

    public String largestComponentformattedString()
    {
        StringBuffer buffer = new StringBuffer(128);
        if (!m_positive)
        {
            buffer.append("-");
        }

        if (m_years != 0)
        {
            if (buffer.length() > 0)
                buffer.append(" ");
            buffer.append(m_years);
            buffer.append((m_years == 1) ? " Year" : " Years");
            return buffer.toString();
        }

        if (m_months != 0)
        {
            if (buffer.length() > 0)
                buffer.append(" ");
            buffer.append(m_months);
            buffer.append((m_months == 1) ? " Month" : " Months");
            return buffer.toString();
        }

        if (m_days != 0)
        {
            if (buffer.length() > 0)
                buffer.append(" ");
            buffer.append(m_days);
            buffer.append((m_days == 1) ? " Day" : " Days");
            return buffer.toString();
        }

        if (m_hours != 0)
        {
            if (buffer.length() > 0)
                buffer.append(" ");
            buffer.append(m_hours);
            buffer.append((m_hours == 1) ? " Hour" : " Hours");
            return buffer.toString();
        }

        if (m_mins != 0)
        {
            if (buffer.length() > 0)
                buffer.append(" ");
            buffer.append(m_mins);
            buffer.append((m_mins == 1) ? " Minute" : " Minutes");
            return buffer.toString();
        }

        if (buffer.length() > 0)
            buffer.append(" ");
        BigDecimal a = new BigDecimal(m_seconds);
        BigDecimal b = new BigDecimal(m_nanos);
        a = a.add(b.divide(new BigDecimal(1000000000), 9, BigDecimal.ROUND_HALF_EVEN));
        NumberFormat nf = NumberFormat.getInstance();
        String formatted = nf.format(a);
        buffer.append(formatted);
        buffer.append(formatted.equals("1") ? " Second" : " Seconds");
        return buffer.toString();

    }

    /**
     * Format in human readable form
     * 
     * TODO: I18n
     */

    public String formattedString()
    {
        StringBuffer buffer = new StringBuffer(128);
        if (!m_positive)
        {
            buffer.append("-");
        }
        if (m_years != 0)
        {
            if (buffer.length() > 0)
                buffer.append(" ");
            buffer.append(m_years);
            buffer.append((m_years == 1) ? " Year" : " Years");

        }
        if (m_months != 0)
        {
            if (buffer.length() > 0)
                buffer.append(" ");
            buffer.append(m_months);
            buffer.append((m_months == 1) ? " Month" : " Months");
        }
        if (m_days != 0)
        {
            if (buffer.length() > 0)
                buffer.append(" ");
            buffer.append(m_days);
            buffer.append((m_days == 1) ? " Day" : " Days");
        }
        if (hasTime())
        {
            if (m_hours != 0)
            {
                if (buffer.length() > 0)
                    buffer.append(" ");
                buffer.append(m_hours);
                buffer.append((m_hours == 1) ? " Hour" : " Hours");
            }
            if (m_mins != 0)
            {
                if (buffer.length() > 0)
                    buffer.append(" ");
                buffer.append(m_mins);
                buffer.append((m_mins == 1) ? " Minute" : " Minutes");
            }
            if ((m_seconds != 0) || (m_nanos != 0))
            {
                if (buffer.length() > 0)
                    buffer.append(" ");
                BigDecimal a = new BigDecimal(m_seconds);
                BigDecimal b = new BigDecimal(m_nanos);
                a = a.add(b.divide(new BigDecimal(1000000000), 9, BigDecimal.ROUND_HALF_EVEN));
                NumberFormat nf = NumberFormat.getInstance();
                String formatted = nf.format(a);
                buffer.append(formatted);
                buffer.append(formatted.equals("1") ? " Second" : " Seconds");
            }

        }

        return buffer.toString();
    }

    /**
     * TODO: Tests that should be moved into a unit test
     * 
     * @param args
     *            String[]
     */
    public static void main(String[] args)
    {
        Duration diff = new Duration("2002-04-02T01:01:01", "2003-03-01T00:00:00");
        System.out.println("Diff  " + diff);

        try
        {
            Duration test = new Duration("P");
            System.out.println("Just P" + test);
        }
        catch (RuntimeException e)
        {
            e.printStackTrace();
        }

        try
        {
            Duration test2 = new Duration("P Jones");
            System.out.println("P Jones" + test2);
        }
        catch (RuntimeException e)
        {
            e.printStackTrace();
        }

        try
        {
            Duration test2 = new Duration("P12Y Jones");
            System.out.println("P Jones" + test2);
        }
        catch (RuntimeException e)
        {
            e.printStackTrace();
        }

        try
        {
            Duration test = new Duration("PPPPPPPPPPPPPP");
            System.out.println("Just many P" + test);
        }
        catch (RuntimeException e)
        {
            e.printStackTrace();
        }

        try
        {
            Duration test = new Duration("PY");
            System.out.println("PY" + test);
        }
        catch (RuntimeException e)
        {
            e.printStackTrace();
        }

        try
        {
            Duration test = new Duration("PM");
            System.out.println("PM" + test);
        }
        catch (RuntimeException e)
        {
            e.printStackTrace();
        }

        try
        {
            Duration test = new Duration("PP");
            System.out.println("PP" + test);
        }
        catch (RuntimeException e)
        {
            e.printStackTrace();
        }

        Date now = new Date();
        Calendar c = Calendar.getInstance();
        c.setTime(now);
        c.add(Calendar.YEAR, -1);
        c.add(Calendar.MONTH, +2);
        c.add(Calendar.DAY_OF_MONTH, -3);
        c.add(Calendar.HOUR_OF_DAY, +4);
        c.add(Calendar.MINUTE, -5);
        c.add(Calendar.SECOND, +6);
        c.add(Calendar.MILLISECOND, -7);

        diff = new Duration(c.getTime(), now);
        System.out.println("V:  " + diff);

        Duration diff2 = new Duration(now, c.getTime());
        System.out.println("V:  " + diff2);

        Duration a1 = new Duration("P2Y6M");
        Duration a2 = new Duration("P1DT2H3M1.5S");

        Duration d = new Duration("P2Y6M5DT12H35M30.100S");
        System.out.println("V:  " + d);
        System.out.println("F:  " + d.formattedString());
        System.out.println(" D: " + d.divide(2));
        System.out.println(" +: " + d.add(a1));
        System.out.println(" +: " + d.add(a1.add(a2)));
        d = new Duration("P1DT2H3M1.5S");
        System.out.println("V:  " + d);
        System.out.println("F:  " + d.formattedString());
        System.out.println(" D: " + d.divide(2));
        System.out.println(" +: " + d.add(a1));
        System.out.println(" +: " + d.add(a1.add(a2)));
        d = new Duration("PT1.5S");
        System.out.println("V:  " + d);
        System.out.println("F:  " + d.formattedString());
        System.out.println(" D: " + d.divide(2));
        System.out.println(" +: " + d.add(a1));
        System.out.println(" +: " + d.add(a1.add(a2)));
        d = new Duration("P20M");
        System.out.println("V:  " + d);
        System.out.println("F:  " + d.formattedString());
        System.out.println(" D: " + d.divide(2));
        System.out.println(" +: " + d.add(a1));
        System.out.println(" +: " + d.add(a1.add(a2)));
        d = new Duration("P0Y20M0D");
        System.out.println("V:  " + d);
        System.out.println("F:  " + d.formattedString());
        System.out.println(" D: " + d.divide(2));
        System.out.println(" +: " + d.add(a1));
        System.out.println(" +: " + d.add(a1.add(a2)));
        d = new Duration("-P60D");
        System.out.println("V:  " + d);
        System.out.println("F:  " + d.formattedString());
        System.out.println(" D: " + d.divide(10));
        System.out.println(" +: " + d.add(a2));
        // System.out.println(" +: " + d.add(a1));

        d = new Duration("P1H");
        System.out.println("V:  " + d);
        System.out.println("F:  " + d.formattedString());
        System.out.println(" D: " + d.divide(10));
        System.out.println(" +: " + d.add(a2));

        d = new Duration("PT1H");
        System.out.println("V:  " + d);
        System.out.println("F:  " + d.formattedString());
        System.out.println(" D: " + d.divide(10));
        System.out.println(" +: " + d.add(a2));

    }
}
